BIOS Programmer's Reference
The system firmware contains a library of procedures which aims to isolate software from the specific details of the underlying hardware. They allow programs to conduct input and output to two types of devices: the user's console, and an external storage medium.
It should be remembered that the B in BIOS stands for basic; the BIOS is not a replacement for an operating system. Applications which invoke BIOS services directly will still have dependencies on the overall nature of Kestrel-2DX hardware resources. Thus, software using BIOS will only be portable between similar revisions of the Kestrel-2DX hardware.
Locating the BiosInterface Table.
Programs which are launched by the BIOS may appear anywhere in the Kestrel-2DX IPL memory space. BIOS itself, however, exists at a fixed location, starting at address 0. Thus, programs that are loaded from external media must be position independent somehow, and must find out for themselves how to access system BIOS.
The current process for locating the system BIOS is to scan memory for a well-defined, 64-bit numeric constant starting at address $10000, and ending at whatever address your program was loaded at. The following assembly language routine will perform this task for current revisions of the Kestrel-2DX hardware:
include "biosdata.i"
; The first thing we need to do is find the BIOS entry points.
; We don't actually know where it is placed in memory, except
; that it will reside at an address less than _start.
_start: auipc s0,0 ; S0 -> _start
addi sp,sp,-8
sd ra,0(sp)
lui a0,$10000 ; Start of ROM+RAM in Kestrel-2DX
ld a2,bs_match-_start(s0)
seek_bios: ld a3,bi_matchword(a0) ; Find it yet?
beq a3,a2,found_bios
addi a0,a0,8 ; Otherwise, try next dword in RAM
blt a0,s0,seek_bios
; If we're here, we did not find the BIOS entry point. Wedge
; hard, as there's nothing else we can do. Your code might somehow
; show a more helpful message on the screen, etc.
wedge: lui a0,$10000
lh a1,0(a0)
addi a1,a1,1
sh a1,0(a0)
jal x0,wedge
; If we've found the BIOS, then let's remember where we found it,
; then kick off the rest of our program.
found_bios: sd a0,bs_bi-_start(s0)
jal x0,start_program
align 8
bs_match: dword BI_MATCHWORD
bs_bi: dword 0
The value of BI_MATCHWORD
is $0BADC0DEB105DA7A
. This value is chosen to appear, probabilistically, virtually never without explicit intent within the relatively tight range of memory being scanned.
Calling BIOS Functions
Once you have a reference to the BiosInterface
table,
you can invoke BIOS functions using a consistent procedure:
First, load parameters into registers A0..A7.
Invoke the desired procedure through the stored pointer.
If necessary, return values will appear in either a0
and/or in a1
.
Alternatively, some BIOS procedures take pointers to variables,
which will receive results upon successful completion of a procedure.
Here's an example where we desire to change the cursor location on the screen:
L1: auipc a1,0 ; A1 -> L1
addi a0,a1,cursor_x - L1 ; A0 -> cursor_x
addi a1,a1,cursor_y - L1 ; A1 -> cursor_y
ld t0,bs_bi - _start(s0) ; T0 -> BiosInterface table
ld t0,bi_term_cursor_swap(t0) ; T0 -> term_cursor_swap entry point
jalr ra,0(t0) ; Call BIOS procedure
; ...
cursor_x: byte 0
cursor_y: byte 0
BIOS Function Reference
This section contains a brief summary of each of the BIOS functions currently implemented, as of ROM version 0.1.0. Each entry is formatted like so:
bios_function_name (bios_interface_offset)
results = bios_function_name(param1, param2, ...)
A0 A0 A1 ...
Description of procedure taken when calling this function,
including results returned.
Storage Procedure Error Codes
Many of the storage-related BIOS procedures returns an error code. These procedures share the same error code space, which can be summarized as follows:
E_OK
(0) — No errors were discovered; the SD card is safe to conduct I/O with.E_UNIT
(-1) — The selected device is not recognized as a valid peripheral.E_CARD
(-2) — The SD card has failed basic protocol checks and/or is missing.E_TIMEOUT
(-3) — The SD card performed valid protocol sequences up until this point, but now is taking much longer than expected to complete some step in the protocol. This can also happen because the SD card is now missing (perhaps because the user pulled it out in the middle of an I/O operation).E_NOT_ACCEPTED
(-4) — The SD card seems to be operating OK, but for some reason a write operation was rejected by the SD card (e.g., perhaps due to incorrect CRC or insufficient privileges).
Terminal Output: Cursor Management
term_cursor_on
(8)
term_cursor_on()
This procedure decrements an internal counter.
If the counter remains non-zero,
no further action is taken.
Otherwise, the cursor will be rendered again.
See also term_cursor_off
.
Note.
After booting into software loaded from external storage,
the cursor will be off by default.
Your software must make a call to term_cursor_on
to render the cursor again.
term_cursor_off
(16)
term_cursor_off()
This procedure turns the cursor off if it's visible, and increments a counter. As long as the counter is non-zero, the cursor will remain off.
For every call to term_cursor_off
,
there must be a corresponding call to term_cursor_on
for the cursor to be visible again.
term_cursor_swap
(24)
term_cursor_swap(uint8_t *px, uint8_t *py)
A0 A1
This procedure exchanges the requested cursor position with the current cursor position.
On entry,
px
is a pointer to a byte holding the desired X coordinate, and
py
is a pointer to a byte holding the desired Y coordinate,
respectively,
of the new cursor position.
If either *px
or *py
are beyond the valid dimensions of the screen,
they will be clipped to the largest value possible.
On return,
*px
will contain the previous cursor X coordinate, and
*py
will contain the previous cursor Y coordinate.
This function can be used in three ways.
To recover the dimensions of the screen, you can attempt to move the cursor to the largest expressible coordinate:
uint8_t x = 255, y = 255;
term_cursor_swap(&x, &y);
term_cursor_swap(&x, &y); // Restores previous position
printf(
"The screen has %d rows and %d columns\n",
y + 1,
x + 1
);
To move the cursor to a new position, simply ignore the results:
uint8_t x = 40, y = 12;
term_cursor_swap(&x, &y);
To query the cursor's current position, you must restore the cursor's previous position.
However, make sure you capture the results of the first call to term_cursor_swap
:
uint8_t x, y;
term_cursor_swap(&x, &y); // Grabs current position.
where.x = x;
where.y = y;
term_cursor_swap(&x, &y); // Restores previous position.
Terminal Output: Displaying Text
term_out_clear
(32)
term_out_clear()
Clears the screen, and homes the cursor to the upper, left-hand corner.
Note. You must turn the cursor off prior to clearing the screen. Clearing the screen will erase the cursor without updating BIOS cursor state. The next time the cursor is moved, turned off, or a character is printed, it will end up corrupting the display by leaving a stray inverse-video character cell (assuming the BIOS implements the cursor as a solid block).
The proper sequence for emitting a character is:
term_cursor_off();
term_out_clear();
term_cursor_on();
term_out_chr
(40)
term_out_chr(char ch)
A0
Prints the provided character ch
to the screen.
Some ASCII control values have certain interpretations which do not result in a graphic character being printed.
Character Code | Result |
---|---|
$07 | The BEL character; since the Kestrel-2DX has no audio facilities, it is ignored. |
$08 | Moves cursor to the left one character. Does not erase the character underneath the cursor, nor does it shift characters to the right. |
$09 | Advance cursor to the next tab-stop. |
$0A | Without altering the cursor's horizontal position, advance to the next line on the screen. Scroll if necessary. |
$0B | Vertical tab; ignored. |
$0C | Clears the screen. |
$0D | Without advancing the cursor to the next line, return the cursor to the far left-hand edge of the screen. |
Tab-stops are located every eight characters on the screen.
All other characters are treated as graphic characters for printing. Note. Printing characters in the set {0..6, 14..31} is not officially supported, for these characters are intended for terminal control purposes. Future versions of the Kestrel-2DX BIOS may interpret more control characters in the future.
Note. You must turn the cursor off prior to printing any characters. The first character thus output will overwrite the cell containing the cursor. If/when the cursor is moved, or subsequently turned off for any other reason, it will end up corrupting the display by leaving a stray inverse-video character (assuming the BIOS implements the cursor as a solid block).
The proper sequence for emitting a character is:
term_cursor_off();
term_out_chr(ch);
term_cursor_on();
term_out_buf
(48)
term_out_buf(char *buf, size_t length)
A0 A1
Outputs a fixed-sized buffer to the screen. Bytes within the buffer are interpreted as 7-bit ASCII with an extended character set for character codes {128..255}.
Character codes {7..13} are treated as control
characters. See term_out_chr
for more details.
The buffer pointed to by buf
may contain NUL characters.
These characters are rendered like any other character.
No more than length
bytes will be printed.
Note. You must turn the cursor off prior to printing any characters. The first character thus output will overwrite the cell containing the cursor. If/when the cursor is moved, or subsequently turned off for any other reason, it will end up corrupting the display by leaving a stray inverse-video character (assuming the BIOS implements the cursor as a solid block).
The proper sequence for emitting a character is:
buf = "Text goes here...";
// ...
term_cursor_off();
term_out_buf(buf, strlen(buf));
term_cursor_on();
term_out_str
(56)
term_out_str(char *buf)
A0
Outputs a NUL-terminated string pointed to by buf
to the screen.
Character codes {6..13} are treated as control
characters. See term_out_chr
for more details.
Character code 0 is considered NUL.
Note. You must turn the cursor off prior to printing any characters. The first character thus output will overwrite the cell containing the cursor. If/when the cursor is moved, or subsequently turned off for any other reason, it will end up corrupting the display by leaving a stray inverse-video character (assuming the BIOS implements the cursor as a solid block).
The proper sequence for emitting a character is:
buf = "Text goes here...";
// ...
term_cursor_off();
term_out_str(buf);
term_cursor_on();
Terminal Input: Keyboard
term_in_pollraw
(64)
term_in_pollraw(uint16_t *praw, int *valid)
A0 A1
This procedure polls the keyboard hardware for a raw scancode.
If a scancode is available,
it is assigned to *praw
and *valid
is set to a non-zero value.
Otherwise, if no data is available yet,
*valid
is set to zero, and the contents of *praw
is undefined.
praw
is a pointer to a 16-bit half-word,
which will hold the scancode if data are available.
valid
is a pointer to a 64-bit dword
which will indicate if the value in praw
is safe to use.
Scancodes are basically PS/2 scan codes, but slightly modified, as follows:
15 | 14 .. 9 | 8 | 7 .. 0 |
---|---|---|---|
R |
0 |
X |
scan code |
where R
is set if the scancode is a key release event.
That is, when pressing a key,
the scancode generated will have R
clear;
meanwhile, when releasing that same key,
R
will be set.
X
is set if the key is an extended key code.
One of the characteristics of the PS/2 keyboard encoding
is that some keys share the same scancode,
for backward compatibility with older 16-bit DOS applications
running on IBM PC/XT or PC/AT computers.
For example, with NUMLOCK
turned off,
pressing 4
on the numeric keypad generates the same scancode
as pressing the cursor-left key, since on earlier keyboards,
the numeric keypad doubled as the cursor movement keypad,
depending on the state of the NUMLOCK
key.
Thus, the X
bit allows interested applications
the opportunity to distinguish between "traditional" and "extended"
keys serving the same intended purpose.
The scan code, of course, corresponds to the PS/2 scan code assigned to the particular key in question.
term_in_rawtoascii
(72)
term_in_rawtoascii(uint16_t raw, char *pascii, int *pvalid)
A0 A1 A2
This procedure attempts to convert the raw keyboard scancode
as returned by term_in_pollraw
, into a corresponding ASCII codepoint.
Unfortunately, not all scancodes correspond to ASCII codepoints.
Thus, this procedure may fail.
raw
is the raw scancode as returned by term_in_pollraw
.
pascii
is a pointer to a byte
which will hold the resulting character if the translation is successful.
pvalid
is a pointer to a dword
which will indicate if the translation succeeded or not.
If *pvalid
is non-zero,
the translation is successful, and
*pascii
will contain a valid ASCII codepoint.
Otherwise, *pascii
will be undefined, and its value must remain untrusted.
Note. Currently, *pascii
is set to zero on entry to this function.
However, do not depend on this behavior;
this is simply an artifact of how it's currently written,
and is not intended to be a formal part of its behavior.
term_in_getmeta
(80)
uint8_t term_in_getmeta()
A0
Returns the current state of the
CTRL and SHIFT flags.
See also term_in_setshift
and
term_in_setctrl
for details on how these flags work.
The resulting byte is a bitmask:
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
0 |
0 |
0 |
0 |
0 |
0 |
C |
S |
where S
represents the current state of the SHIFT flag,
and C
represents the current state of the CTRL flag.
All other bits are explicitly reserved for future use, and must be ignored by the caller for upward compatibility.
term_in_setshift
(88)
term_in_setshift(int state)
A0
If state
is non-zero,
assert the SHIFT flag.
This causes all future ASCII graphic characters
submitted to term_in_rawtoascii
to be treated as uppercase or,
in the case of numerals,
punctuation
characters instead.
If state
is zero,
restore normal, lowercase graphic character
handling semantics.
term_in_setctrl
(96)
term_in_setctrl(int state)
A0
If state
is non-zero,
assert the CTRL flag.
This causes all future ASCII graphic characters
submitted to term_in_rawtoascii
to be treated as control characters instead.
If state
is zero,
restore normal graphic character handling semantics.
NOTE. The CTRL flag overrules the SHIFT flag. Thus, there is no difference between CTRL-A and ** CTRL-SHIFT-A; both will result in a character code of $01.
External Storage
sdmmc_writeblock
(104)
err = sdcard_writeblock(block, buffer)
A0 A0 A1
Copies a 512-byte block of data from the buffer provided in buffer
to the storage sector named in block
.
sdmmc_readblock
(112)
err = sdcard_readblock(block, buffer)
A0 A0 A1
Copies a 512-byte block of data from the sector number provided in block
,
to the buffer supplied in buffer
.
The caller is responsible for ensuring that buffer
is big enough to hold at least 512 bytes.
sdmmc_idle
(120)
err = sdmmc_idle()
A0
Performs an SD protocol initialization and idling sequence for the card, if any card exists in the slot. If, for any reason, a problem were to occur during this procedure, an appropriate error result is returned. It is vitally important that no I/O to the SD card be performed if an error is returned from this procedure. Doing so may result in abnormal system behavior and/or data loss, both in memory and on the SD card itself.
sdmmc_is_present
(128)
flag = sdmmc_is_present()
A0
Returns non-zero if an SD card is inserted into the slot; 0 otherwise.
spi_select
(136)
err = spi_select(device)
A0 A0
Addresses a specific SPI device for I/O. Currently, only device 0 is supported (the SD card slot). Devices 1..3 are accepted, but do not address any specific hardware in the stock Kestrel-2DX configuration.
Returns E_OK
if given a valid device ID; otherwise, E_UNIT
is returned.
spi_deselect
(144)
spi_deselect()
Deselects all SPI devices.
Rationale
Why Scan for BiosInterface
?
The RISC-V JAL instruction only covers a 2MB range of addresses relative to the JAL
instruction itself. A future Kestrel design may well place RAM resources further than 2MB away from where the BIOS ROM sits. Thus, software cannot reliably use the JAL
instruction to invoke BIOS services directly. It can, however, load a base address into a temporary register, typically t0
, and invoke services through that using JALR
.
Why Not Use ECALL
?
When it was first written, the BIOS consisted of two parts: a raw assembly bootstrap routine, and the bulk of the functionality written in C. No means currently exists of informing the assembler where the C component's _start
address resides. Further, I have had, and continue to have, great difficulty convincing GNU ld
to place the .text
segment where I desire it. If I had control over this, this whole problem could have been avoided. Alas, it refuses to place .text
ahead of .data
, and so here we are. Thus, just like application software loaded from external storage, it must scan memory to find this entry point. Thus, one must wonder what value to set the mtvec
register to handle machine-mode traps with? While this is a solvable problem, at the time, I considered this approach easier to get working sooner.
One more note on this: using ECALL
necessarily means that mtvec
would need very careful handling if a loaded operating system wanted to use ECALL
itself to handle system calls. It would, in effect, circumvent BIOS all-together, leaving the BIOS with no clear way of being invoked, except through a process of chaining. Careful coordination between BIOS and the operating system would be needed to ensure BIOS and the OS could evolve independently of each other.
Why Start Scan at Address $10000?
The BIOS code necessarily incorporates this constant as a value within its ROM. If scanning starting at address 0, then application programs will find the instance in ROM, not in RAM. The ROM-resident table of pointers to BIOS functions have not been properly relocated, and so calling them directly will only crash your software.
Starting the scan at address $10000 ensures
that scanning starts at the beginning of RAM,
where the BIOS will have copied and properly relocated all the pointers within the BiosInterface
table.